rcgit

/ karakeep

plain blame

import React from "react";
import {
  Keyboard,
  Pressable,
  Text,
  TouchableWithoutFeedback,
  View,
} from "react-native";
import Animated, {
  useAnimatedKeyboard,
  useAnimatedStyle,
} from "react-native-reanimated";
import { router, Stack, useLocalSearchParams } from "expo-router";
import TagPill from "@/components/bookmarks/TagPill";
import FullPageError from "@/components/FullPageError";
import FullPageSpinner from "@/components/ui/FullPageSpinner";
import { Input } from "@/components/ui/Input";
import { Skeleton } from "@/components/ui/Skeleton";
import { ChevronRight } from "lucide-react-native";

import {
  useAutoRefreshingBookmarkQuery,
  useUpdateBookmark,
} from "@karakeep/shared-react/hooks/bookmarks";
import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks";
import { isBookmarkStillTagging } from "@karakeep/shared/utils/bookmarkUtils";

function TagList({ bookmark }: { bookmark: ZBookmark }) {
  return (
    <View className="flex gap-4">
      <Text className="text-lg text-foreground">Tags</Text>
      <View className="flex gap-2">
        {isBookmarkStillTagging(bookmark) ? (
          <View className="flex gap-4 pb-3">
            <Skeleton className="h-4 w-full" />
            <Skeleton className="h-4 w-full" />
          </View>
        ) : bookmark.tags.length > 0 ? (
          <View className="flex flex-row flex-wrap gap-2 rounded-lg bg-background p-4">
            {bookmark.tags.map((t) => (
              <TagPill key={t.id} tag={t} />
            ))}
          </View>
        ) : (
          <Text className="text-foreground">No tags</Text>
        )}
        <Pressable
          onPress={() =>
            router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`)
          }
          className="flex w-full flex-row justify-between gap-3 rounded-lg bg-white px-4 py-2 dark:bg-accent"
        >
          <Text className="text-lg text-accent-foreground">Manage Tags</Text>
          <ChevronRight color="rgb(0, 122, 255)" />
        </Pressable>
      </View>
    </View>
  );
}

function ManageLists({ bookmark }: { bookmark: ZBookmark }) {
  return (
    <View className="flex gap-4">
      <Text className="text-lg text-foreground">Lists</Text>
      <Pressable
        onPress={() =>
          router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`)
        }
        className="flex w-full flex-row justify-between gap-3 rounded-lg bg-white px-4 py-2 dark:bg-accent"
      >
        <Text className="text-lg text-accent-foreground">Manage Lists</Text>
        <ChevronRight color="rgb(0, 122, 255)" />
      </Pressable>
    </View>
  );
}

function TitleEditor({
  bookmarkId,
  title,
}: {
  bookmarkId: string;
  title: string;
}) {
  const { mutate, isPending } = useUpdateBookmark();
  return (
    <View className="flex gap-4">
      <Text className="text-lg text-foreground">Title</Text>

      <Input
        editable={!isPending}
        multiline={true}
        numberOfLines={1}
        loading={isPending}
        placeholder="Title"
        textAlignVertical="top"
        onEndEditing={(ev) =>
          mutate({
            bookmarkId,
            title: ev.nativeEvent.text ? ev.nativeEvent.text : null,
          })
        }
        defaultValue={title ?? ""}
      />
    </View>
  );
}

function NotesEditor({ bookmark }: { bookmark: ZBookmark }) {
  const { mutate, isPending } = useUpdateBookmark();
  return (
    <View className="flex gap-4">
      <Text className="text-lg text-foreground">Notes</Text>

      <Input
        editable={!isPending}
        multiline={true}
        numberOfLines={3}
        loading={isPending}
        placeholder="Notes"
        textAlignVertical="top"
        onEndEditing={(ev) =>
          mutate({
            bookmarkId: bookmark.id,
            note: ev.nativeEvent.text,
          })
        }
        defaultValue={bookmark.note ?? ""}
      />
    </View>
  );
}

const ViewBookmarkPage = () => {
  const { slug } = useLocalSearchParams();
  if (typeof slug !== "string") {
    throw new Error("Unexpected param type");
  }

  const keyboard = useAnimatedKeyboard();

  const animatedStyles = useAnimatedStyle(() => ({
    marginBottom: keyboard.height.value,
  }));

  const {
    data: bookmark,
    isPending,
    refetch,
  } = useAutoRefreshingBookmarkQuery({
    bookmarkId: slug,
  });

  if (isPending) {
    return <FullPageSpinner />;
  }

  if (!bookmark) {
    return (
      <FullPageError error="Bookmark not found" onRetry={() => refetch()} />
    );
  }

  let title = null;
  switch (bookmark.content.type) {
    case BookmarkTypes.LINK:
      title = bookmark.title ?? bookmark.content.title;
      break;
    case BookmarkTypes.TEXT:
      title = bookmark.title;
      break;
    case BookmarkTypes.ASSET:
      title = bookmark.title ?? bookmark.content.fileName;
      break;
  }
  return (
    <View>
      <Stack.Screen
        options={{
          headerShown: true,
          headerTransparent: false,
          headerTitle: title ?? "Untitled",
          headerRight: () => (
            <Pressable
              onPress={() => {
                if (router.canGoBack()) {
                  router.back();
                } else {
                  router.replace("dashboard");
                }
              }}
            >
              <Text className="text-foreground">Done</Text>
            </Pressable>
          ),
        }}
      />
      <Animated.ScrollView className="p-4" style={[animatedStyles]}>
        <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
          <View className="h-screen gap-4 px-2">
            <TitleEditor bookmarkId={bookmark.id} title={title ?? ""} />
            <TagList bookmark={bookmark} />
            <ManageLists bookmark={bookmark} />
            <NotesEditor bookmark={bookmark} />
          </View>
        </TouchableWithoutFeedback>
      </Animated.ScrollView>
    </View>
  );
};

export default ViewBookmarkPage;